/* Bazinga - MPD Jukebox
 * Copyright (C) 2010 m3Soft (Martin Sieke, Matthias Sieke, Matthias Bockstegers)
 * 
 * This program is distributed under the terms of the GNU General Public License.
 * You can redistribute  and/or modify it under the terms of the GNU General Public License 
 * as published by the Free Software Foundation, either version 3 of the License, 
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,but WITHOUT ANY WARRANTY; 
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 * See the GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program.  
 * If not, see http://www.gnu.org/licenses
 */

package de.m3soft.bazinga;

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableModel;

import org.bff.javampd.MPD;
import org.bff.javampd.exception.MPDConnectionException;
import org.bff.javampd.exception.MPDDatabaseException;
import org.bff.javampd.exception.MPDException;
import org.bff.javampd.exception.MPDPlayerException;
import org.bff.javampd.exception.MPDPlaylistException;
import org.bff.javampd.objects.MPDAlbum;
import org.bff.javampd.objects.MPDArtist;
import org.bff.javampd.objects.MPDSong;

public class BazingaGUI extends JFrame {

	private static final long serialVersionUID = 1L; // unified identnumber for class

	private MPD mpd;

	private Methoden methoden;

	private int songSelectedPlaylist = 1; // row of selected song in playlist

	private int songSelectedResult = 0; // row of selected song in searchResults

	private boolean adminModeEnabled = true; // State of admin mode (set default on startup HERE)

	// define all the buttons (read title for description)
	private JButton BTnext = new JButton(new ImageIcon("img/forward.png"));

	private JButton BTstop = new JButton(new ImageIcon("img/stop.png"));

	private JButton BTplay = new JButton(new ImageIcon("img/play.png"));

	private JButton BTdelete = new JButton(new ImageIcon("img/delete.png"));

	// TODO fix the funtion that is inoked by BTplaySong and add the button to the panel again (s. another todo)
	private JButton BTplaySong = new JButton("play track from results");

	private JButton BTsearch = new JButton("search");

	private JButton BTsearchYT = new JButton("search the internet");

	private JCheckBox CBadmin = new JCheckBox("admin");

	private JButton BTabout = new JButton("about");

	// Label displays album-cover-art
	private JLabel imageLabel = new JLabel();

	// textField for the adminPassword
	private JPasswordField adminPasswordField = new JPasswordField(10);

	// define datamodel for Playlist Table and searchresult Table
	private DefaultTableModel dataModelPlaylist = new DefaultTableModel();

	private DefaultTableModel dataModelSearchResults = new DefaultTableModel();

	// define the Playlist Table (adding the dataModel)
	JTable playlistTable = new JTable(dataModelPlaylist) {
		private static final long serialVersionUID = 1L;

		// make it uneditable
		public boolean isCellEditable(int x, int y) {
			return false;
		};
	};

	// define the SearchResult Table (adding the dataModel)
	JTable searchResultTable = new JTable(dataModelSearchResults) {
		private static final long serialVersionUID = 1L;

		// make it uneditable
		public boolean isCellEditable(int x, int y) {
			return false;
		};
	};

	// create scrollpanes to make the tables scrollable
	JScrollPane scrollpanePlaylist = new JScrollPane(playlistTable);

	JScrollPane scrollpaneSearchResults = new JScrollPane(searchResultTable);

	private JPanel TOP_LEFT = new JPanel(new GridLayout(2, 2)); // contains admin buttons
	private JPanel TOP_LEFT_MIDDLE = new JPanel(new BorderLayout()); // contains admin buttons
	private JPanel TOP_CENTER_COVER = new JPanel(new BorderLayout()); // contains admin buttons
	private JPanel TOP_RIGHT_MIDDLE = new JPanel(new BorderLayout()); // contains admin buttons

	private JPanel TOP_CENTER = new JPanel(new BorderLayout()); // contains song-info and song progress

	private JPanel TOP_RIGHT = new JPanel(new GridLayout(1, 2)); // contains admin activation checkbox,

	private JPanel TOP_RIGHT_adminAndAbout = new JPanel(new GridLayout(2, 1)); // albumArt and Aboutbox

	// Define Panel and Components of the TOP region
	private JPanel TOP = new JPanel(new FlowLayout()); // container for all the components on the TOP

	// private JTextArea state = new JTextArea("", 6, 40); // Title, Artist and Album of current track
	private JLabel state = new JLabel();
	private JLabel spacer1 = new JLabel();
	private JLabel spacer2 = new JLabel();

	private JProgressBar progress = new JProgressBar(); // Progress of current track

	// Define Panel and Components of the CENTER region
	private JTabbedPane jTabCENTER = new JTabbedPane(); // container of all the tabbed panes in the middle

	private JPanel BUTTONandField = new JPanel(new FlowLayout()); // container for search field and search button above the result table

	private JTextField searchText = new JTextField(30); // search field itself

	private JPanel RESULTS = new JPanel(new BorderLayout()); // 

	// Define Textfield for statusbar at the bottom (no panel needed)
	private JTextField consoleBottom = new JTextField();

	// Constructor
	public BazingaGUI(MPD mpd) throws UnknownHostException, MPDException, MPDConnectionException, MPDDatabaseException, MPDPlaylistException, MPDPlayerException {

		this.mpd = mpd;
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); // window can be closed
		// by admin only
		setVisible(false); // start invisible
		setTitle("Bazinga!");
		setLayout(new BorderLayout()); // set Layout for main frame

		// Method for setting the properties of all components (names, editable)
		setComponentProperties();

		// Add Windowlistener to main frame, ButtonListener to buttons and so on
		addAllActionlisteners();

		// Add components to panels and then add all frames to main frame
		frameContent();

		// Set Admin mode according to initial value of adminMode boolean
		changeAdminStuff(adminModeEnabled);

	}

	// set Attributes of all the components such as names of table columns and setEditable states
	private void setComponentProperties() {
		// jtable "playlist" is just to display so there are just two columbs
		dataModelPlaylist.addColumn("Title:");
		dataModelPlaylist.addColumn("Artist:");
		dataModelPlaylist.addColumn("Album:");

		// the third column in the jtable "searchResults" is not
		// displayed (minWidth=maxWidh=0) and contains the track object
		// makes it possible to add tracks of the result table
		dataModelSearchResults.addColumn("Title:");
		dataModelSearchResults.addColumn("Artist:");
		dataModelSearchResults.addColumn("Album:");
		dataModelSearchResults.addColumn("Schmuu:");
		searchResultTable.getColumnModel().getColumn(3).setMinWidth(0);
		searchResultTable.getColumnModel().getColumn(3).setMaxWidth(0);

		adminPasswordField.setEchoChar('*');

		scrollpanePlaylist.setName("playlist");
		RESULTS.setName("search");

		// Song description must not be editable
		// state.setEditable(false);
		state.setOpaque(false);
		state.setSize(560, 104);
		spacer1.setText("  ");
		spacer2.setText("  ");
		BTdelete.setName("img/delete.png");
		BTnext.setName("img/forward.png");
		BTstop.setName("img/stop.png");
		BTplay.setName("img/play.png");
		
		BTsearch.setName("search");
		BTsearchYT.setName("search the internet");
		BTabout.setName("about");
		
		
		// Status-Text-output (bottom) must not be editable
		consoleBottom.setEditable(false);
	}

	// sets size of window according to properties of cfg file
	public void setWindowSize() {
		// set position and size of main frame
		// by setting coordinates of all four edges)
		int x, y;
		x = Integer.parseInt(methoden.getProperty("BoundsX")) + 0;
		y = Integer.parseInt(methoden.getProperty("BoundsY")) + 0;
		setBounds(0, 0, x, y);
	}

	// adds Actionlistener such as Buttonlistener, mouse and keybord listener
	private void addAllActionlisteners() {
		// add windowListener to the main frame so that the window can be closed
		// properly
		addWindowListener(new Beender());

		// Add the ButtonListener to all the buttons in the top frame
		BTplay.addActionListener(new ButtonListener());
		BTsearch.addActionListener(new ButtonListener());
		BTsearchYT.addActionListener(new ButtonListener());
		BTstop.addActionListener(new ButtonListener());
		BTnext.addActionListener(new ButtonListener());
		BTdelete.addActionListener(new ButtonListener());
		BTplaySong.addActionListener(new ButtonListener());
		CBadmin.addItemListener(new ButtonListener());
		BTabout.addActionListener(new ButtonListener());

		// enable search on enter key stroke
		searchText.addKeyListener(new searchKeyListener());

		// enable song add on doubleClick
		searchResultTable.addMouseListener(new SongAdder());
		playlistTable.getSelectionModel().addListSelectionListener(new PlaylistMarker());
	}

	// adds Components to JPanels and all the panels to the main frame
	private void frameContent() {
		// create two suport panels to prevent to gridlayout from resizing AboutButton and adminCeckbox
		JPanel TOP_RIGHT_AboutButton = new JPanel(new FlowLayout());
		JPanel TOP_RIGHT_adminBox = new JPanel(new FlowLayout());

		// Add Buttons to Frame in the upper left corner
		TOP_LEFT.add(BTplay);
		TOP_LEFT.add(BTnext);
		TOP_LEFT.add(BTstop);
		TOP_LEFT.add(BTdelete);
		TOP_LEFT_MIDDLE.add(spacer1);
		TOP_RIGHT_MIDDLE.add(spacer2);
		TOP_CENTER_COVER.add(imageLabel);
		// TODO TOP.add(BTplaySong); doesn't work yet

		// Add song description and songProgress to upper center panel
		TOP_CENTER.add(state, BorderLayout.CENTER);
		TOP_CENTER.add(progress, BorderLayout.SOUTH);

		// Add aboutButton, adminCheckBox and albumArt to upper right corner
		// (aboutButton and adminCheckBox in extra flow panels for better look)
		TOP_RIGHT_AboutButton.add(BTabout);
		TOP_RIGHT_adminBox.add(CBadmin);
		TOP_RIGHT_adminAndAbout.add(TOP_RIGHT_AboutButton);
		TOP_RIGHT_adminAndAbout.add(TOP_RIGHT_adminBox);
		TOP_RIGHT.add(TOP_RIGHT_adminAndAbout);

		// add all three upper panels
		TOP.add(TOP_LEFT);
		TOP.add(TOP_LEFT_MIDDLE);
		TOP.add(TOP_CENTER_COVER);
		TOP.add(TOP_CENTER);
		TOP.add(TOP_RIGHT_MIDDLE);
		TOP.add(TOP_RIGHT);

		// Group search field and button into one frame
		BUTTONandField.add(searchText);
		BUTTONandField.add(BTsearch);
		// TODO BUTTONandField.add(BTsearchYT); //doesn't work yet
		BUTTONandField.add(BTsearchYT);
		// add them to the RESULT tab (top)
		RESULTS.add(BUTTONandField, BorderLayout.NORTH);
		// add the search searchResult table to the result tab (middle)
		RESULTS.add(scrollpaneSearchResults, BorderLayout.CENTER);

		jTabCENTER.add(scrollpanePlaylist);
		jTabCENTER.add(RESULTS);

		// add upper middle and lower panels to main frame
		add(TOP, BorderLayout.NORTH);
		add(jTabCENTER, BorderLayout.CENTER);
		add(consoleBottom, BorderLayout.SOUTH);
	}

	public void searchSong(String s) {
		printSearchResults(methoden.findSong(s));
	}

	// Method sets Visibility of Admin Components according to admin state
	private void changeAdminStuff(boolean isAdmin) {
		BTplay.setVisible(isAdmin);
		BTnext.setVisible(isAdmin);
		BTstop.setVisible(isAdmin);
		BTdelete.setVisible(isAdmin);
		BTplaySong.setVisible(isAdmin);
		// state of checkbox is deselected somehow so it has to be set everytime
		CBadmin.setSelected(isAdmin);
		this.repaint();
		TOP.repaint();
	}

	// simple set methode for methode object
	public void setMethoden(Methoden methoden) {
		this.methoden = methoden;
	}

	// methode uses search methode to find songs and updates ResultTable
	public void updatePlaylist() throws MPDPlaylistException, MPDConnectionException {
		Vector<String> vec = new Vector<String>();
		dataModelPlaylist.setRowCount(0);
		ArrayList<String[]> youtubeSongs = null;
		if (methoden != null)
			youtubeSongs = methoden.getYoutubeSongs();

		for (MPDSong o : mpd.getMPDPlaylist().getSongList()) {
			if (o.getFile().contains("4shared.com") && youtubeSongs != null) {
				for (String[] ytSong : youtubeSongs) {
					if (o.getFile().equalsIgnoreCase(ytSong[0])) {
						o.setAlbum(new MPDAlbum("Youtube"));
						o.setArtist(new MPDArtist("Youtube"));
						o.setTitle(ytSong[1]);
						break;
					}
				}
			}
			if (o.getFile().equals("http://bootlog.dyndns.org/bazinga.mp3")) {
				for (String[] ytSong : youtubeSongs) {
					if (o.getFile().equalsIgnoreCase(ytSong[0])) {
						o.setAlbum(new MPDAlbum("YouPorn"));
						o.setArtist(new MPDArtist("Bazinga! Team"));
						o.setTitle("Bazinga! From Nerdistan with Love");
						break;
					}
				}
			}

			vec = new Vector<String>();
			vec.add(methoden.getTitle(o));
			vec.add(methoden.getArtist(o).getName());
			vec.add(methoden.getAlbum(o).getName());
			dataModelPlaylist.addRow(vec);

		}

		this.repaint();
	}

	// function sets value of progressbar according to elapsed duration of track (outch my english...)
	public void setRemain(int songLength, Long elapsed) {
		long temp = elapsed;
		int i = (int) temp;
		progress.setMaximum(songLength);
		progress.setValue(i);

	}

	// sets statustext at the bottom
	public void setConsole(String s) {
		consoleBottom.setText(s);
	}

	// sets statustext on top (invoked by several Playlist and Trackchange Listeners)
	public void updateState(MPDSong s) {
		if (s != null)
			// state.setText("Song: \t" + methoden.getTitle(s) + "\n" + "Artist: \t" + methoden.getArtist(s) + "\n" + "Album: \t" + methoden.getAlbum(s) + "\n"
			// + "Genre: \t" + s.getGenre() + "\n");
			state.setText("<html><div style=\"white-space:nowrap; overflow: hidden; width: 400px; overflow: hidden;\"><p style=\"line-height: 2; font-size: 13px;width: 400px; overflow: hidden; align: left;\">" 
					+ methoden.getArtist(s) + "</p><p style=\"font-size: 16px; width: 400px; overflow: hidden; align: left;\">" 
					+ methoden.getTitle(s) + "</p><p style=\"width: 400px; overflow: hidden; align: left;\"><i>" + methoden.getAlbum(s) 
					+ "</i></p><br /></div></html>");
	}

	// function clears the searchResults and shows the new list
	public void printSearchResults(List<MPDSong> l) {
		Vector<Object> vec = new Vector<Object>();
		dataModelSearchResults.setRowCount(0);
		for (MPDSong s : l) {
			vec = new Vector<Object>();
			vec.removeAllElements();
			vec.add(methoden.getTitle(s));
			vec.add(methoden.getArtist(s));
			vec.add(methoden.getAlbum(s));
			vec.add(s); // 3rd column contains song as object / not displayed
			dataModelSearchResults.addRow(vec);
		}
		this.repaint();
	}

	// Listener listens to all the Buttons and the admin checkbox and runs methode according to event source
	private class ButtonListener implements ActionListener, ItemListener {
		public void actionPerformed(ActionEvent e) {

			// which button was clicked??
			JButton r = (JButton) e.getSource();
			String name = r.getName();
			
			
			if (name.equals("search"))
				printSearchResults(methoden.findSong(searchText.getText()));
			// youtTube search
			if (name.equals("search the internet"))
				// notification("Sorry!", "This funktion is not yet enabled, due to instabilities.", "warning");
				printSearchResults(methoden.youtubeSearch(searchText.getText()));
			// Play pause toggle (admin only)
			if (name.equals("img/play.png")) {
				methoden.play();
			}
			// play next (admin only)
			if (name.equals("img/forward.png"))
				methoden.playNext();
			// stop player (admin only)
			if (name.equals("img/stop.png"))
				methoden.stopPlaying();
			// delete marked track from playlist (admin only)
			if (name.equals("img/delete.png"))
				methoden.removeSong(songSelectedPlaylist);
			// play marked track from searchResults at once (admin only)
			if (name.equals("play track from results")) {
				MPDSong song = getSongAt(songSelectedResult);
				notification("Playlist changed: ", methoden.addSong(song), "added");
				song.setPosition(0);
			}
			if (name.equals("about")) {
				JDialog f = new Aboutbox(new JFrame());
				f.setVisible(true);
			}
		}

		// Eventlistener Method for adminCheckbox
		public void itemStateChanged(ItemEvent e) {
			if ((e.getStateChange() == ItemEvent.SELECTED) && !adminModeEnabled) {
				JCheckBox j = (JCheckBox) e.getSource();

				JOptionPane.showMessageDialog(null, adminPasswordField, "Enter password", JOptionPane.OK_OPTION);

				// Check password:
				// correct -> enable adminMode
				// incorrect -> reset checkox
				String pass = new String(adminPasswordField.getPassword());
				if (methoden.checkPassword(pass))
					adminModeEnabled = true;
				else
					j.setSelected(false);

			}
			// deactivate admin mode without pass
			if (e.getStateChange() == ItemEvent.DESELECTED)
				adminModeEnabled = false;
			// set visibilities of adminButtons according to state of adminMode
			changeAdminStuff(adminModeEnabled);
			// clear admin pass-field
			adminPasswordField.setText("");
		}
	}

	// Listener recognizes marking of list elements in Playlist Table and saves the number
	private class PlaylistMarker implements ListSelectionListener {
		public void valueChanged(ListSelectionEvent lse) {
			songSelectedPlaylist = lse.getFirstIndex();
		}
	}

	// SongAdder reacts on doubleClicks on ResultTable and adds the Tracks to Playlist
	private class SongAdder implements MouseListener {

		public void mouseClicked(MouseEvent e) {
			if (e.getClickCount() == 2) {
				songSelectedResult = searchResultTable.rowAtPoint(e.getPoint());
				notification("Playlist changed: ", methoden.addSong(getSongAt(songSelectedResult)), "added");
			}
			if (e.getClickCount() == 1)
				songSelectedResult = searchResultTable.rowAtPoint(e.getPoint());
		}

		public void mouseEntered(MouseEvent arg0) {
		}

		public void mouseExited(MouseEvent arg0) {
		}

		public void mousePressed(MouseEvent arg0) {
		}

		public void mouseReleased(MouseEvent arg0) {
		}

	}

	// methode converts PlaylistIndex to Song
	private MPDSong getSongAt(int i) {
		return (MPDSong) searchResultTable.getValueAt(i, 3);
	}

	// invokes a dialogBox with a String
	private void showBox(String s) {
		JOptionPane.showMessageDialog(this, s);
	}

	// KeyListener enables the start of Database search by pressing Enter
	private class searchKeyListener implements KeyListener {

		public void keyTyped(KeyEvent e) {
		}

		public void keyReleased(KeyEvent e) {
		}

		public void keyPressed(KeyEvent ke) {
			if (ke.getKeyCode() == KeyEvent.VK_ENTER) {
				printSearchResults(methoden.findSong(searchText.getText()));

			}

		}

	}

	private class Beender extends WindowAdapter {
		public void windowClosing(WindowEvent we) {
			if (adminModeEnabled) {
				methoden.saveConfig();
				dispose();
				System.exit(0);
			} else
				notification("Fehler", "Fenster darf nur vom admin geschlossen werden!", "warning");
		}
	}

	/*
	 * This method diplays messages via libnotify or MethodBox as stated in the settings
	 */
	public void notification(String title, String message) {
		notification(title, message, "warning");
	}

	public void notification(String title, String message, String type) {
		if (Boolean.parseBoolean(methoden.getProperty("libnotify"))) {
			String icon = null, artist = "", album = "", titel = "";

			if (type.equalsIgnoreCase("added"))
				icon = System.getProperty("user.dir") + "/" + "img/cdplus.png";
			else if (type.equalsIgnoreCase("warning"))
				icon = System.getProperty("user.dir") + "/" + "img/cdwait.png";
			else if (type.equalsIgnoreCase("playing")) {
				try {
					ArrayList<String[]> youtubeSongs = null;
					MPDSong currentSong = mpd.getMPDPlayer().getCurrentSong();
					if (methoden != null)
						youtubeSongs = methoden.getYoutubeSongs();

					if (currentSong.getFile().contains("4shared.com") && youtubeSongs != null) {
						for (String[] ytSong : youtubeSongs) {
							if (currentSong.getFile().equalsIgnoreCase(ytSong[0])) {
								currentSong.setAlbum(new MPDAlbum("Youtube"));
								currentSong.setArtist(new MPDArtist("Youtube"));
								currentSong.setTitle(ytSong[1]);
								break;
							}
						}
					}

					if (currentSong.getFile().equals("http://bootlog.dyndns.org/bazinga.mp3")) {
						for (String[] ytSong : youtubeSongs) {
							if (currentSong.getFile().equalsIgnoreCase(ytSong[0])) {
								currentSong.setAlbum(new MPDAlbum("YouPorn"));
								currentSong.setArtist(new MPDArtist("Bazinga! Team"));
								currentSong.setTitle("Bazinga! From Nerdistan with Love");
								break;
							}
						}
					}

					titel = methoden.getTitle(currentSong);
					artist = methoden.getArtist(currentSong).getName();
					album = methoden.getAlbum(currentSong).getName();
					icon = System.getProperty("user.dir") + "/" + methoden.getAlbumArt(artist, album);
					title = "Now Playing:";
					message = titel + "\n" + artist + " (" + album + ")";
				} catch (MPDPlayerException e1) {
				} catch (MPDConnectionException e1) {
				}
			}

			try {
				new ProcessBuilder("notify-send", "-t", "4000", title, message, "-i", icon).start();
			} catch (IOException e) {
				showBox("error using libnotify - " + title + " " + message);
			}
		} else {
			showBox(title + "\n" + message);
		}
	}

	// Runs the CoverGet function and refreshes the Picture
	public void updateAlbumArt(String artist, String album) {
		if (artist == null || album == null || methoden == null)
			return;

		int h, w;
		String imageFile = null;
		ImageIcon ico = null;

		imageFile = methoden.getAlbumArt(artist, album);
		if (imageFile != null && !imageFile.equals("")) {
			ico = new ImageIcon(this.getClass().getClassLoader().getResource(imageFile));
		} else
			return;

		w = 100 * ico.getIconWidth() / ico.getIconHeight();
		h = 100;

		// System.out.println("h: " + ico.getIconHeight() + " w: " +
		// ico.getIconWidth());

		ico.setImage(ico.getImage().getScaledInstance(w, h, Image.SCALE_SMOOTH));
		imageLabel.setIcon(ico);
		this.repaint();

	}

	private class Aboutbox extends JDialog {
		private static final long serialVersionUID = 1L;

		public Aboutbox(JFrame parent) {
			super(parent, "About Bazinga!", true);

			Box b = Box.createVerticalBox();

			String about = "<html><table cellspacing=\"15\"><td><tr>" + "<b> <font size=\"7\"> Bazinga! v0.9.5</font> </b>" + "<br /><br />Bazinga! is an easy to use, yet semi stable java " + "mpd animal<br />with Jukebox flavor.<br /><br />mpd " + "(media player deamon) means:<ul><li> multiple network " + "access from different workstations </li> <li> music allways playing," + " even if client crashes </li> <li> user rights management</li></ul>" + "Jukebox flavor means: <ul><li> no more skipping</li> <li>" + "restrictions in: <ul> <li> total number of tracks in playlist </li> <li>  " + "tracks per artist </li> <li> tracks per album </li></ul>   <li> fading out " + "of endless tracks</li></ul>Bazinga! is developed and tested for " + "Ubuntu Linux.<br /><br />It uses the API of " + "<a href=\"http://www.thejavashop.net/javampd/index.shtml\">" + "http://www.thejavashop.net/javampd/index.shtml</a><br />" + "<br />For more information on MPD check out " + "<a href=\"http://www.musicpd.org/\">http://www.musicpd.org/</a><br />        </tr>" + "<td><tr><table></html>";
			b.add(new JLabel(about));
			b.add(Box.createGlue());
			getContentPane().add(b, "Center");

			JPanel p2 = new JPanel();
			JButton ok = new JButton("Ok");
			p2.add(ok);
			getContentPane().add(p2, "South");

			ok.addActionListener(new ActionListener() {
				public void actionPerformed(ActionEvent evt) {
					setVisible(false);
				}
			});
			setBounds(250, 150, 500, 640);
		}
	}
}
